const { spawn, spawnSync } = require('child_process')
const os = require('os')
const fs = require('fs-extra')
const path = require('path')
const { ZipFile } = require('yazl')
const { open } = require('yauzl')
const openZipFile = open

var diffBin
try {
  diffBin = 'utils\\diff\\bsdiff.exe'
  if ('linux' === os.type()) {
    diffBin = 'utils/diff/bsdiff'
  }
  diff = (origin, next) => {
    fs.mkdirsSync('temp')
    fs.createFileSync('temp/origin')
    fs.writeFileSync('temp/origin', origin)
    fs.createFileSync('temp/next')
    fs.writeFileSync('temp/next', next)
    fs.createFileSync('temp/output')
    const commandResult = spawnSync(
      `${diffBin} temp\/origin temp\/next temp\/output `,
      {
        windowsVerbatimArguments: true,
        shell: true,
        stdio: 'inherit'
      }
    )
    console.log(
      commandResult.status,
      commandResult.stdout,
      commandResult.stderr,
      commandResult.output,
      commandResult.signal
    )
    if (commandResult.error) {
      throw commandResult.error
    }
    var buffer = fs.readFileSync('temp/output')
    fs.unlinkSync('temp/output')
    fs.unlinkSync('temp/origin')
    fs.unlinkSync('temp/next')
    fs.rmdirSync('temp')
    return buffer
  }
} catch (e) {
  diff = function() {
    console.warn(
      'This function needs "node-bsdiff". Please run "npm i node-bsdiff" from your project directory first!'
    )
    throw new Error(
      'This function needs module "node-bsdiff". Please install it first.'
    )
  }
}

function exec(command) {
  const commandResult = spawnSync(command, {
    shell: true,
    stdio: 'inherit'
  })
  if (commandResult.error) {
    throw commandResult.error
  }
}

function readEntire(entry, zipFile) {
  const buffers = []
  return new Promise((resolve, reject) => {
    zipFile.openReadStream(entry, (err, stream) => {
      stream.pipe({
        write(chunk) {
          buffers.push(chunk)
        },
        end() {
          resolve(Buffer.concat(buffers))
        },
        prependListener() {},
        on() {},
        once() {},
        emit() {}
      })
    })
  })
}

function basename(fn) {
  const m = /^(.+\/)[^\/]+\/?$/.exec(fn)
  return m && m[1]
}

function enumZipEntries(zipFn, callback) {
  return new Promise((resolve, reject) => {
    openZipFile(zipFn, { lazyEntries: true }, (err, zipfile) => {
      if (err) {
        return reject(err)
      }
      zipfile.on('end', resolve)
      zipfile.on('error', reject)
      zipfile.on('entry', entry => {
        const result = callback(entry, zipfile)
        if (result && typeof result.then === 'function') {
          result.then(() => zipfile.readEntry())
        } else {
          zipfile.readEntry()
        }
      })
      zipfile.readEntry()
    })
  })
}

const diffFromPackage = async function(
  origin,
  next,
  output,
  originBundleName,
  transformPackagePath = v => v
) {
  fs.ensureDirSync(path.dirname(output))

  const originEntries = {}
  const originMap = {}

  let originSource

  await enumZipEntries(origin, (entry, zipFile) => {
    if (!/\/$/.test(entry.fileName)) {
      const fn = transformPackagePath(entry.fileName)
      if (!fn) {
        return
      }

      //console.log(fn);
      // isFile
      originEntries[fn] = entry.crc32
      originMap[entry.crc32] = fn

      if (fn === originBundleName) {
        // This is source.
        return readEntire(entry, zipFile).then(v => (originSource = v))
      }
    }
  })

  originSource = originSource || new Buffer(0)

  const copies = {}

  var zipfile = new ZipFile()

  const writePromise = new Promise((resolve, reject) => {
    zipfile.outputStream.on('error', err => {
      throw err
    })
    zipfile.outputStream
      .pipe(fs.createWriteStream(output))
      .on('close', function() {
        resolve()
      })
  })

  await enumZipEntries(next, (entry, nextZipfile) => {
    // console.log(entry.fileName)
    if (/\/$/.test(entry.fileName)) {
      // Directory
      zipfile.addEmptyDirectory(entry.fileName)
    } else if (entry.fileName === 'assets/index.android.bundle') {
      //console.log('Found bundle');
      return readEntire(entry, nextZipfile).then(newSource => {
        //console.log('Begin diff');
        zipfile.addBuffer(diff(originSource, newSource), 'index.bundlejs.patch')
        //console.log('End diff');
      })
    } else if (
      (entry.fileName.indexOf('res/drawable') === 0 &&
        entry.fileName.indexOf('-v4/') > 0) ||
      entry.fileName.indexOf('raw/') === 0
    ) {
      // If same file.
      if (originEntries[entry.fileName] === entry.crc32) {
        if (entry.fileName.indexOf('-v4/') > 0) {
          copies[entry.fileName.replace('-v4/', '/').replace('res/', '')] =
            entry.fileName
        } else {
          copies[entry.fileName] = ''
        }
        return
      }
      // If moved from other place
      if (originMap[entry.crc32]) {
        if (entry.fileName.indexOf('-v4/') > 0)
          copies[entry.fileName.replace('-v4/', '/').replace('res/', '')] =
            originMap[entry.crc32]
        else {
          copies[entry.fileName] = originMap[entry.crc32]
        }
        return
      }

      return new Promise((resolve, reject) => {
        nextZipfile.openReadStream(entry, function(err, readStream) {
          if (err) {
            return reject(err)
          }
          zipfile.addReadStream(readStream, entry.fileName)
          readStream.on('end', () => {
            //console.log('add finished');
            resolve()
          })
        })
      })
    }
  })

  zipfile.addBuffer(new Buffer(JSON.stringify({ copies })), '__diff.json')
  zipfile.end()
  await writePromise
}

/**
 * 对比Apk生成react-native 差分包
 * @param {*} origin
 * @param {*} next
 * @param {*} output
 */
const diffFromApk = async (origin, next, output) => {
  await diffFromPackage(origin, next, output, 'assets/index.android.bundle')
  console.log(`${output} generated.`)
}

module.exports = { diffFromPackage, diffFromApk }

// diffFromApk('1.apk', '2.apk', 'output')
